package com.xcode.unit.builder;

import com.google.common.collect.Lists;
import com.squareup.javapoet.*;
import com.xcode.unit.config.UnitTestConfig;
import com.xcode.unit.constant.UnitTestClassConstants;
import com.xcode.unit.model.FieldModel;
import com.xcode.unit.model.MethodModel;
import com.xcode.unit.model.SourceClassModel;
import com.xcode.unit.util.LFileUtil;
import com.xcode.unit.util.LReflectionUtils;
import com.xcode.unit.util.LStringUtils;
import com.xcode.unit.xclassloader.XDynamicClassLoader;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl;

import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.FileSystems;
import java.util.*;

/**
 * @author liuziying
 * @Description 构建单元测试，通过解析Java源代码结构
 * @Date 2020/12/30
 */
public class LUnitTestBuilder {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    private SourceClassModel sourceClassModel;
    private TypeSpec.Builder classBuilder;
    private JavaFile.Builder builder;
    private List<String> expceptions = Lists.newArrayList("Exception", "ClassNotFoundException", "NoSuchMethodException","IndexOutOfBoundsException", "UnknownHostException","IOException");
    private UnitTestConfig unitTestConfig;
    private XDynamicClassLoader classLoader;

    private LUnitTestBuilder(){}

    public static LUnitTestBuilder builder() {
        return new LUnitTestBuilder();
    }

    public LUnitTestBuilder addSourceClassModel(SourceClassModel sourceClassModel){
        this.sourceClassModel = sourceClassModel;
        return this;
    }

    public LUnitTestBuilder configUnitTest(UnitTestConfig unitTestConfig){
        this.unitTestConfig = unitTestConfig;
        if(this.classLoader == null){
            logger.info("开始解压!");
            String unZipPath = LFileUtil.getNewFileDir(unitTestConfig.getOutDir() + File.separator + LFileUtil.getFileName(unitTestConfig.getJarFile()).replace(".jar",""));
            LFileUtil.unjar(unitTestConfig.getJarFile(), unZipPath);
            logger.info("解压完成 -> {}",unZipPath);
            unitTestConfig.setClassPath(unZipPath);
            unitTestConfig.setLibPath(unZipPath);
            this.classLoader = new XDynamicClassLoader(unitTestConfig.getClassPath() + File.separator, unitTestConfig.getLibPath());
        }
        return this;
    }

    public String getMethodReturnType(String fieldName, String methodName, int parameterCount){
        FieldModel fieldModel = sourceClassModel.getFieldModel(fieldName);
        final String clazzPathName = sourceClassModel.getImportGeneralByName(fieldModel.getType());
        Class<?> clazz = classLoader.findClass(clazzPathName);
        final String methodType = LReflectionUtils.getMethodType(clazz, methodName, parameterCount);
        return methodType;
    }

    public void create(){
        if(ObjectUtils.isEmpty(sourceClassModel)){
            throw new IllegalArgumentException("请先添加源代码模型");
        }

        /*
        初始化类
         */
        String testClassName = sourceClassModel.getClassName() + UnitTestClassConstants.NAME_SUFFIX_TEST;
        classBuilder = TypeSpec.classBuilder(testClassName);

        this.buildTestClass()
                .buildMockField()
                .buildMockOptionalField()
                .buildStub()
                .writeToFile();
        logger.info("成功生成单元测试类： {}.java  路径 -> {}{}{}", testClassName, unitTestConfig.getOutDir(), File.separator, sourceClassModel.getPackageName().replace(".", File.separator));
    }

    /**
     * 获取方法块
     * @param methodModel
     * @return
     */
    private CodeBlock getCodeBlock(MethodModel methodModel){
        Queue<String> bodyQueue = methodModel.getBodyQueue();
        CodeBlock.Builder builder = CodeBlock.builder();
        while (bodyQueue.size() > 0){
            String ele = bodyQueue.poll();
            builder.addStatement("$L", ele);
//            if(ele.contains("@")){
//                String[] formatArgs = ele.split("@");
//                String[] args = formatArgs[1].split(",");
//                builder.addStatement(formatArgs[0], args);
//            }else {
//                builder.addStatement("$L", ele);
//            }
        }
        return builder.build();
    }

    /**
     * 待完善 TODO
     * @param methodModel
     * @return
     */
    private List<TypeName> getExceptions(MethodModel methodModel){
        List<TypeName> exps = Lists.newArrayList();
        List<String> exceptionTypes = methodModel.getExceptionTypes();
        if(unitTestConfig.isPowerMock()){
            exps.add(ParameterizedTypeName.get(Exception.class));
        }
        exceptionTypes.forEach(exceptionType -> {
            if(expceptions.contains(exceptionType)){
                exps.add(ParameterizedTypeName.get(Exception.class));
            }
        });
        return exps;
    }

    private LUnitTestBuilder buildStub(){
        List<MethodSpec> methodSpecs = Lists.newArrayList();
        Collection<MethodModel> methodModels = sourceClassModel.getMethodModels().values();
        methodModels.forEach(methodModel -> {
            methodSpecs.add(MethodSpec.methodBuilder(methodModel.getMethodName() + UnitTestClassConstants.NAME_SUFFIX_TEST).returns(void.class)
                    .addAnnotation(AnnotationSpec.builder(Test.class).build())
                    .addCode(getCodeBlock(methodModel).toBuilder().build())
                    .addModifiers(Modifier.PUBLIC)
                    .addExceptions(getExceptions(methodModel))
                    .build());
        });

        if(!CollectionUtils.isEmpty(methodSpecs)){
            classBuilder.addMethods(methodSpecs);
        }
        return this;
    }

    /**
     * 可选Mock字段
     * @return
     */
    private LUnitTestBuilder buildMockOptionalField(){
        List<FieldSpec> generalFields = Lists.newArrayList();
        FieldSpec thrownFiled = FieldSpec.builder(ExpectedException.class, UnitTestClassConstants.THROWN)
                .initializer("$L","ExpectedException.none()")
                .addAnnotation(AnnotationSpec.builder(Rule.class).build())
                .addModifiers(Modifier.PUBLIC)
                .build();
        generalFields.add(thrownFiled);

        FieldSpec podamFactoryFiled = FieldSpec.builder(PodamFactory.class, UnitTestClassConstants.PODAM_FACTORY)
                .initializer("new $T()", PodamFactoryImpl.class)
                .build();
        generalFields.add(podamFactoryFiled);

        classBuilder.addFields(generalFields);
        return this;
    }

    /**
     * 类和注入字段Mock
     * @return
     */
    private LUnitTestBuilder buildMockField(){
        List<FieldSpec> generalFields = Lists.newArrayList();
        Class<?> clazz = classLoader.findClass(sourceClassModel.getPackageName() + "." +sourceClassModel.getClassName());
        FieldSpec testClassNameVar = FieldSpec.builder(clazz,
                LStringUtils.toLowerCaseFirst(sourceClassModel.getClassName()))
                .addAnnotation(AnnotationSpec.builder(InjectMocks.class).build()).build();
        generalFields.add(testClassNameVar);

        //获取类字段
        List<FieldModel> fieldModels = sourceClassModel.getFieldModels();
        for(FieldModel fieldModel : fieldModels){
            Type variableType = LReflectionUtils.getVariableType(fieldModel.getName(), clazz);
            if(variableType == null){
                logger.warn("Mock字段：{} 异常", fieldModel.getName());
                continue;
            }
            generalFields.add(FieldSpec.builder(variableType, LStringUtils.toLowerCaseFirst(fieldModel.getName()))
                    .addAnnotation(AnnotationSpec.builder(Mock.class).build()).build());
        }

        classBuilder.addFields(generalFields);
        return this;
    }

    /**
     * 构建类信息
     * @return
     */
    private LUnitTestBuilder buildTestClass(){
        //构建类信息
        String testClassName = sourceClassModel.getClassName() + UnitTestClassConstants.NAME_SUFFIX_TEST;
        classBuilder = TypeSpec.classBuilder(testClassName);
        classBuilder.addModifiers(Modifier.PUBLIC);
        if(unitTestConfig.isPowerMock()){
            classBuilder.addAnnotation(AnnotationSpec.builder(RunWith.class)
                    .addMember("value","$T.class", PowerMockRunner.class).build());
            classBuilder.addAnnotation(AnnotationSpec.builder(PrepareForTest.class)
                    .addMember("value","$L", sourceClassModel.getClassName()+".class").build());
        }else{
            classBuilder.addAnnotation(AnnotationSpec.builder(RunWith.class)
                    .addMember("value","$T.class", MockitoJUnitRunner.class).build());
        }
        return this;
    }

    private LUnitTestBuilder buildJavaFile(){
        builder = JavaFile.builder(sourceClassModel.getPackageName(), classBuilder.build());
        builder.addStaticImport(Assert.class,"*");
        builder.addStaticImport(Mockito.class,"*");
        if(unitTestConfig.isPowerMock()){
            builder.addGeneralImport("org.powermock.reflect.Whitebox");
            builder.addGeneralImport("org.powermock.api.mockito.PowerMockito");
        }
        builder.addGeneralImport(sourceClassModel.getPackageName()+"."+sourceClassModel.getClassName());
        Map<String, String> importGenerals = sourceClassModel.getImportGenerals();
        importGenerals.values().forEach(importGeneral -> {
            builder.addGeneralImport(importGeneral);
        });
        Map<String, String> importStatics = sourceClassModel.getImportStatics();
        importStatics.values().forEach(importStatic -> {
            builder.addGeneralImport("static " + importStatic);
        });

        return this;
    }

    private void writeToFile(){
        try {
            //Java文件生成
            buildJavaFile();
            JavaFile javaFile = builder.build();
            if(ObjectUtils.isEmpty(this.unitTestConfig.getOutDir())){
                javaFile.writeTo(FileSystems.getDefault().getPath("src/test/java/"));
            }else {
                javaFile.writeTo(new File(this.unitTestConfig.getOutDir()));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
